﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.NetworkInformation;
using System.Threading;
using System.Windows.Forms;
using System.Xml.Serialization;
using HttpHandlerSpace;
using Microsoft.Win32;
using System.Text.RegularExpressions;

namespace JetLoginTool.Engine
{
	/// <summary>
	/// Deals with Jet
	/// </summary>
	public class JetEngine : IDisposable
	{
		#region Constants
		// URL that is known to be external to the Uni
		const string EXTERNAL_URL = "www.google.com.au";

		// login stuff for the Jet form
		const string LOGIN_FORM_USERNAME = "username";
		const string LOGIN_FORM_PASSWORD = "password";
		const string LOGIN_FORM_HIDDEN = "targeturl";
		const string LOGIN_FORM_SUBMIT = "submit";
		const string LOGIN_FORM_SUBMIT_VALUE = "Logon";
		#endregion
		#region Fields
		private static readonly Regex sessionRegex = new Regex("Used this session: (.*?)<br>", RegexOptions.Compiled);
		private static readonly Regex monthRegex = new Regex("Used this month: (.*?)<br>", RegexOptions.Compiled);
		private static readonly Regex remainsRegex = new Regex("You currently have (.*?) remaining", RegexOptions.Compiled);
		private readonly HttpHandler myHttpHandler = new HttpHandler {Timeout = 5000};
		private Thread workerThread;
		private readonly AutoResetEvent loginEvent = new AutoResetEvent(false);
		private readonly ManualResetEvent stopWorker = new ManualResetEvent(false);
		private readonly WaitHandle[] eventArray;

		private bool isLoggedInToJet = false;

		private JetSettings mySettings;
		private int forcedLoginRetries = 0;
		private readonly object forcedLoginRetriesSync;
		#endregion
		#region Constructors
		/// <summary>
		/// Default Constructor
		/// </summary>
		public JetEngine(JetSettings inSettings)
		{
			eventArray = new WaitHandle[] { loginEvent, stopWorker };
			mySettings = inSettings;
			mySettings.CredentialsChanged += NotifyWorkerEventHandler;
			mySettings.LocationChanged += NotifyWorkerEventHandler;
			UsageSession = "";
			UsageMonth = "";
			UsageRemains = "";
			registerForNetworkEvents();
			SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
			forcedLoginRetriesSync = (object)forcedLoginRetries;
		}
		#endregion
		#region Properties
		/// <summary>
		/// The Last response code from Jet
		/// </summary>
		public JetResponseCodes ResponseCode { get; private set; }

		/// <summary>
		/// If the Engine is busy
		/// </summary>
		public bool IsBusy
		{
			get
			{
				return ((workerThread != null) && (workerThread.IsAlive));
			}
		}

		/// <summary>
		/// Amount of data used in the month
		/// </summary>
		public string UsageMonth { get; private set; }

		/// <summary>
		/// Amount of data that remains
		/// </summary>
		public string UsageRemains { get; private set; }

		/// <summary>
		/// Amount of data used this session
		/// </summary>
		public string UsageSession { get; private set; }
		#endregion
		#region Static Methods
		/// <summary>
		/// Converts Jet's response into the appropriate response code
		/// </summary>
		/// <param name="inResponse">HTML response from JET</param>
		/// <returns>The response code, otherwise JetResponseCode.UnknownError</returns>
		public static JetResponseCodes JetResponseStringToResponseCode(string inResponse)
		{
			Dictionary<string, JetResponseCodes> responseStringToCodes = new Dictionary<string, JetResponseCodes>
																			{
																				{"Login Accepted", JetResponseCodes.Success},
																				{"Your login is being processed", JetResponseCodes.JetProcessing},
																				{"Username or password is incorrect",JetResponseCodes.BadUsernamePassword},
																				{"inactive", JetResponseCodes.JetBroken}
																			};

			JetResponseCodes response = JetResponseCodes.UnknownError;

			foreach (string testString in responseStringToCodes.Keys)
			{
				if (inResponse.Contains(testString))
				{
					response = responseStringToCodes[testString];
				}
			}

			return response;
		}
		#endregion
		#region Private Methods
		private void registerForNetworkEvents()
		{
			NetworkChange.NetworkAddressChanged += NotifyWorkerEventHandler;
			NetworkChange.NetworkAvailabilityChanged += NotifyWorkerEventHandler;
		}

		private void LogoutFromJet()
		{
			if (isLoggedInToJet)
			{
				myHttpHandler.clearCookies();
				try
				{
					myHttpHandler.getResponseAsyncStart(myHttpHandler.nextRequest(mySettings.CurrentLocation.LogoutUrl), new HttpHandler.AsyncResponseCallback(LogoutFromJetCallback));
				}
				catch (SystemException)
				{ }
			}
		}

		public void LogoutFromJetCallback(HttpWebResponse response)
		{
			if (response != null)
			{
				string responseText = HttpHandler.responseToString(response);

				if (responseText == "You have been successfully logged out of the network")
				{
					isLoggedInToJet = false;
				}
				responseText = null;
			}
		}

		private JetResponseCodes LoadRefreshText()
		{
			isLoggedInToJet = false;
			JetResponseCodes response;

			try
			{
				string refreshText = HttpHandler.responseToString(myHttpHandler.getResponse(myHttpHandler.nextRequest(mySettings.CurrentLocation.JetPopupTextUrl)));

				if (refreshText.Contains("No cookie provided, cannot process request"))
				{
					myHttpHandler.clearCookies();
					response = LoginToJet();
				}
				else
				{
					Match sessionMatch = sessionRegex.Match(refreshText);
					Match monthMatch = monthRegex.Match(refreshText);
					Match remainsMatch = remainsRegex.Match(refreshText);

					if ((sessionMatch.Success) && (monthMatch.Success) && (remainsMatch.Success) && (sessionMatch.Groups[1].Length > 0) && (monthMatch.Groups[1].Length > 0) && (remainsMatch.Groups[1].Length > 0))
					{
						UsageSession = sessionMatch.Groups[1].Value;
						UsageMonth = monthMatch.Groups[1].Value;
						UsageRemains = remainsMatch.Groups[1].Value;

						response = JetResponseCodes.Success;
						isLoggedInToJet = true;
					}
					else if (!stopWorker.WaitOne(0))
					{
						response = LoginToJet();
					}
					else
					{
						response = JetResponseCodes.UserRequestedStop;
					}

					sessionRegex.Match(string.Empty);
					monthRegex.Match(string.Empty);
					remainsRegex.Match(string.Empty);
				}

				refreshText = null;
			}
			catch (WebException)
			{
				response = JetResponseCodes.NetworkError;
			}
			catch (Exception)
			{
				response = JetResponseCodes.UnknownError;
			}

			return response;
		}

		private bool CanLoadInternalNetwork()
		{
			bool canLoad = false;

			try
			{
				HttpWebRequest myRequest = myHttpHandler.nextRequest(mySettings.CurrentLocation.InternalUrl);
				myHttpHandler.getResponse(myRequest);
				if ((myRequest.Address == mySettings.CurrentLocation.InternalUrl) && (myRequest.HaveResponse))
				{
					canLoad = true;
				}
			}
			catch (WebException)
			{ }

			return canLoad;
		}

		private bool CanLoadExternalNetwork()
		{
			bool canLoad = false;

			try
			{
				if (SocketHandler.CanHttpConnect(EXTERNAL_URL))
				{
					canLoad = true;
				}
			}
			catch (SystemException)
			{ }

			return canLoad;
		}

		private JetResponseCodes LoginToJet()
		{
			isLoggedInToJet = false;
			JetResponseCodes response;
			Dictionary<string, string> postVars = new Dictionary<string, string>
													{
														{LOGIN_FORM_HIDDEN, ""},
														{LOGIN_FORM_USERNAME, mySettings.Username},
														{LOGIN_FORM_PASSWORD, mySettings.PlainPassword},
														{LOGIN_FORM_SUBMIT, LOGIN_FORM_SUBMIT_VALUE}
													};

			

			try
			{
				string refreshText = HttpHandler.responseToString(myHttpHandler.getResponse(myHttpHandler.nextRequest(mySettings.CurrentLocation.JetLoginUrl, postVars)));

				if (refreshText.Contains("Login Accepted"))
				{
					isLoggedInToJet = true;
					if (stopWorker.WaitOne(0))
					{
						response = JetResponseCodes.UserRequestedStop;
					}
					else
					{
						response = LoadRefreshText();
					}
				}
				else if (refreshText.Contains("Username or password is incorrect"))
				{
					response = JetResponseCodes.BadUsernamePassword;
				}
				else if (refreshText.Contains("Your Login is Being Processed"))
				{
					response = JetResponseCodes.JetProcessing;
				}
				else
				{
					response = JetResponseCodes.JetBroken;
				}

				refreshText = null;
			}
			catch (WebException)
			{
				response = JetResponseCodes.NetworkError;
			}
			catch (Exception)
			{
				response = JetResponseCodes.UnknownError;
			}

			return response;
		}

		private void NotifyWorker()
		{
			lock (forcedLoginRetriesSync)
			{
				if (forcedLoginRetries < 2)
				{
					forcedLoginRetries++;
				}
			}
			loginEvent.Set();
		}
		#endregion
		#region Public Methods
		/// <summary>
		/// Begins the Auto-Login Process
		/// </summary>
		public bool startAutoLogin()
		{
			bool autoLoginStarted = false;

			if ((workerThread == null) || (!workerThread.IsAlive))
			{
				ResponseCode = JetResponseCodes.None;
				stopWorker.Reset();
				loginEvent.Reset();
				isLoggedInToJet = false;
				forcedLoginRetries = 1;
				workerThread = new Thread(new ThreadStart(ThreadDoWork)) { IsBackground = true };
				workerThread.Start();
				autoLoginStarted = true;
			}

			return autoLoginStarted;
		}
		
		/// <summary>
		/// Cancels the Auto-Login Process
		/// </summary>
		public void stopAutoLogin()
		{
			lock (forcedLoginRetriesSync)
			{
				forcedLoginRetries = int.MinValue;
			}
			stopWorker.Set();
			LogoutFromJet();
		}
		#endregion
		#region EventHandlers
		void NotifyWorkerEventHandler(object sender, EventArgs e)
		{
			NotifyWorker();
		}

		void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
		{
			if (e.Mode == PowerModes.Resume)
			{
				registerForNetworkEvents();
				NotifyWorker();
			}
		}

		private void ThreadDoWork()
		{
			int timeout = 300000;

			do
			{
				/* Steps to do:
			 *	Are we on the Internet?
			 *		yes) Are we logged in to Jet?
			 *			yes) Refresh
			 *			no) notify that internet ok, not logged in
			 *		no) Can we see the internal network?
			 *			yes) Can we log in to Jet?
			 *				yes) Refresh
			 *				no) Network error
			 *			no) Wait for the network
			 */

				bool loginForced = false;
				lock (forcedLoginRetriesSync)
				{
					if (forcedLoginRetries > 0)
					{
						forcedLoginRetries--;
						loginForced = true;
						loginEvent.Reset();
					}
				}
				JetResponseCodes response = ResponseCode;

				if (NetworkInterface.GetIsNetworkAvailable())
				{
					RaiseProcessingStarted();
					if (loginForced)
					{
						response = LoginToJet();
					}

					if ((!loginForced) || (!isLoggedInToJet))
					{
						if (CanLoadExternalNetwork())
						{
							response = LoadRefreshText();

							if (isLoggedInToJet)
							{
								response = JetResponseCodes.Success;
							}
							else
							{
								response = JetResponseCodes.InternetOkNotLoggedIn;
							}
						}
						else
						{
							if (CanLoadInternalNetwork())
							{
								response = LoginToJet();
							}
							else
							{
								response = JetResponseCodes.WaitingForNetwork;
							}
						}
					}
					RaiseProcessingCompleted();
				}
				else
				{
					response = JetResponseCodes.WaitingForNetwork;
				}

				if (response != ResponseCode)
				{
					ResponseCode = response;
					RaiseResponseCodeChangedOccured();
				}

				switch (response)
				{
					case JetResponseCodes.Success:
					case JetResponseCodes.WaitingForNetwork:
						timeout = 300000;
						break;
					case JetResponseCodes.NetworkError:
						timeout = 2000;
						if (loginForced)
						{
							lock (forcedLoginRetriesSync)
							{
								forcedLoginRetries++;
							}
						}
						break;
					default:
						timeout = 60000;
						break;
				}
			} while ((ResponseCode != JetResponseCodes.BadUsernamePassword) && (ResponseCode != JetResponseCodes.UnknownError) && ((forcedLoginRetries > 0) || (WaitHandle.WaitAny(eventArray, timeout) != 1)));

			RaiseJetEngineHalted();
		}
		#endregion
		#region Events
		/// <summary>
		/// Occurs when the JetLogin has returned from being called
		/// </summary>
		public event EventHandler ResponseCodeChanged;
		private void RaiseResponseCodeChangedOccured()
		{
			if (ResponseCodeChanged != null)
			{
				ResponseCodeChanged.BeginInvoke(this, new EventArgs(), null, null);
			}
		}
		
		/// <summary>
		/// Occurs when something causes the Jet Engine to halt ro stop
		/// </summary>
		public event EventHandler JetEngineHalted;
		private void RaiseJetEngineHalted()
		{
			if (JetEngineHalted != null)
			{
				JetEngineHalted.BeginInvoke(this, new EventArgs(), null, null);
			}
		}

		/// <summary>
		/// Occurs when the worker thread starts running through its cycle
		/// </summary>
		public event EventHandler ProcessingStarted;
		private void RaiseProcessingStarted()
		{
			if (ProcessingStarted != null)
			{
				ProcessingStarted.BeginInvoke(this, new EventArgs(), null, null);
			}
		}

		/// <summary>
		/// Occurs when the worker thread has finished running through its cycle
		/// </summary>
		public event EventHandler ProcessingCompleted;
		private void RaiseProcessingCompleted()
		{
			if (ProcessingCompleted != null)
			{
				ProcessingCompleted.BeginInvoke(this, new EventArgs(), null, null);
			}
		}
		#endregion
		#region IDisposable Members
		~JetEngine()
		{
			Dispose(false);
		}

		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (disposing)
			{
				try
				{
					workerThread.Abort();
					workerThread.Join();
				}
				catch (NullReferenceException)
				{
				}
				
				((IDisposable)loginEvent).Dispose();
				((IDisposable)stopWorker).Dispose();
			}

			SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged;
		}
		#endregion
	}

	/// <summary>
	/// Enumeration of Possible Responses from Jet\JetEngine
	/// </summary>
	public enum JetResponseCodes
	{
		/// <summary>
		/// Empty for the flags enum
		/// </summary>
		None,
		/// <summary>
		/// Successful response from Jet
		/// </summary>
		Success,
		/// <summary>
		/// Unknown Error - You shouldn't see this
		/// </summary>
		UnknownError,
		/// <summary>
		/// There was a network error
		/// </summary>
		NetworkError,
		/// <summary>
		/// Incorrect Username or Password provided
		/// </summary>
		BadUsernamePassword,
		/// <summary>
		/// Jet is not working today...
		/// </summary>
		JetBroken,
		/// <summary>
		/// Jet says "Your Login is Being Processed" - could mean incorrect password
		/// </summary>
		JetProcessing,
		/// <summary>
		/// Internet is working, but Jet isn't logged in
		/// </summary>
		InternetOkNotLoggedIn,
		/// <summary>
		/// Can't see external network
		/// </summary>
		WaitingForNetwork,
		/// <summary>
		/// There is an issue with the stored cookie for jet (bad, expired, invalid, etc)
		/// </summary>
		JetCookieBad,
		/// <summary>
		/// User has hit the stop button, so process has been stopped
		/// </summary>
		UserRequestedStop,
	}
}
